package com.frontop.terminal.rmonclient.client;

import com.frontop.terminal.entity.TerminalConfig;
import com.frontop.terminal.rmonclient.encrypt.MD5;
import com.frontop.terminal.rmonclient.hid.HIDCommand;
import com.frontop.terminal.rmonclient.hid.KeyboardCommand;
import com.frontop.terminal.rmonclient.hid.MouseCommand;
import com.frontop.terminal.rmonclient.protocol.Command;
import com.frontop.terminal.rmonclient.protocol.Packet;
import com.frontop.terminal.rmonclient.system.FileSystem;
import com.frontop.terminal.rmonclient.util.ByteUtils;
import com.frontop.terminal.rmonclient.util.Configs;
import com.frontop.terminal.rmonclient.util.Log;
import com.frontop.terminal.rmonclient.util.Nonce;
import com.frontop.terminal.rmonclient.worker.BaseWorker;
import com.frontop.terminal.rmonclient.worker.CaptureWorker;
import com.frontop.terminal.rmonclient.worker.CompressWorker;
import com.frontop.terminal.rmonclient.worker.FileTransferWorker;
import com.frontop.terminal.rmonclient.worker.HIDCommandExecutor;
import com.frontop.terminal.rmonclient.worker.HeartBeatWorker;
import com.frontop.terminal.rmonclient.worker.ScreenImages;
import com.frontop.terminal.util.ReadDataUtil;
import com.frontop.terminal.util.SystemMonitoringUtil;
import lombok.extern.slf4j.Slf4j;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * @author ccc
 */
@SuppressWarnings("AlibabaUndefineMagicConstant")
@Slf4j
public class Client extends Thread {
    private static final byte[] TAIL = new byte[]{(byte) 0xfa, (byte) 0xfa, (byte) 0xfa};
    /**
     * 是否正在发送截图
     */
    boolean working = false;

    /**
     * 认证状态
     */
    boolean authenticated = false;

    /**
     * 捕获的工作者
     */
    BaseWorker captureWorker;

    /**
     * 压缩的工作者
     */
    BaseWorker compressWorker;

    /**
     * 鼠标键盘事件
     */
    HIDCommandExecutor hidCommandExecutor;

    /**
     * 心跳
     */
    HeartBeatWorker heartBeatWorker;
    Socket conn;
    DataInputStream inputStream;
    DataOutputStream outputStream;
    long lastActiveTime = 0L;
    private long restBytesToReceive = 0;
    private FileOutputStream fileWriter = null;
    private BufferedOutputStream bufferedFileWriter = null;

    /**
     * 与服务器间的会话处理
     *
     * @throws Exception Exception
     */
    private void converse() throws Exception {
        //获取配置
        TerminalConfig terminalConfig = ReadDataUtil.getTerminalConfig();
        if (Objects.isNull(terminalConfig)) {
            return;
        }

        working = false;
        conn = new Socket(terminalConfig.getIp(), 19122);
        conn.setSoTimeout(5000);
        conn.setKeepAlive(true);
        inputStream = new DataInputStream(conn.getInputStream());
        outputStream = new DataOutputStream(conn.getOutputStream());

        lastActiveTime = System.currentTimeMillis();
        Log.info("Connected to server...");


        // 1. 身份验证

        //获取mac
        String mac = SystemMonitoringUtil.getMacAddress();
        //机器名字
        String clientName = ReadDataUtil.getTerminalConfig().getAlias();
        if (clientName.length() > 20) {
            throw new RuntimeException("受控端名称不能超过20个字符");
        }

        clientName = clientName + ";" + mac;

        byte[] clientNameBytes = clientName.getBytes(StandardCharsets.UTF_8);
        Packet packet = Packet.create(Command.AUTHENTICATE, 64 + clientNameBytes.length + 4);
        packet.addInt(clientNameBytes.length);
        packet.addBytes(clientNameBytes);
        String nonce = Nonce.generate(32);
        packet.addBytes(nonce.getBytes());
        packet.addBytes(Objects.requireNonNull(MD5.encode(nonce + ":::" + Configs.CLIENT_PUBLIC_KEY)).getBytes());

        send(packet);
        heartBeatWorker = new HeartBeatWorker(this);
        heartBeatWorker.start();

        while (System.currentTimeMillis() - lastActiveTime <= 30000) {
            // 有无下发下来的数据包
            packet = Packet.read(inputStream);
            if (packet != null) {
                lastActiveTime = System.currentTimeMillis();
                processCommand(packet);
                continue;
            }

            // 处理服务器下发的指令
            // 有无需要上报的截图
            if (ScreenImages.hasCompressedScreens()) {
                lastActiveTime = System.currentTimeMillis();
                sendScreenImages();
                continue;
            }
            // 如果闲置超过20秒，则发送一个心跳包
            if (System.currentTimeMillis() - lastActiveTime > 3000) {
                Packet p = Packet.create(Command.HEARTBEAT, 5);
                p.addBytes("HELLO".getBytes());
                send(p);
                lastActiveTime = System.currentTimeMillis();
            }
            sleep(5);
        }
        Log.info("Connection closed...");
    }

    /**
     * 处理服务器端下发的指令
     *
     * @param packet 数据包
     * @throws Exception Exception
     */
    @SuppressWarnings("AlibabaMethodTooLong")
    private void processCommand(Packet packet) throws Exception {
        packet.skip(6);
        int cmd = packet.nextByte();
        packet.nextInt();
        Packet resp = null;
        System.out.println("远程socket收到命令：" + cmd);
        if (cmd != Command.AUTHENTICATE_RESPONSE && !authenticated) {
            return;
        }
        if (cmd == Command.AUTHENTICATE_RESPONSE) {
            if (packet.nextByte() == 0x00) {
                authenticated = true;
            } else {
                Log.info("会话认证失败");
                System.exit(1);
            }
        }
        // 开始远程控制
        else if (cmd == Command.CONTROL_REQUEST) {
            System.out.println("开始远程控制");
            if (working) {
                throw new RuntimeException("Already working on capture screenshots...");
            }
            working = true;

            // TODO: 暂不响应服务器端的控制请求的细节要求，比如压缩方式、带宽、颜色位数等
            packet.nextByte();
            packet.nextByte();
            packet.nextByte();
            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            resp = Packet.create(Command.CONTROL_RESPONSE, 15)
                    // 压缩方式
                    .addByte((byte) 0x01)
                    // 带宽
                    .addByte((byte) 0x00)
                    // 颜色位数
                    .addByte((byte) 0x03)
                    // 屏幕宽度
                    .addShort((short) screenSize.getWidth())
                    // 屏幕高度
                    .addShort((short) screenSize.getHeight())
                    // 当前系统时间戳
                    .addLong(System.currentTimeMillis());
            (captureWorker = new CaptureWorker()).start();
            (compressWorker = new CompressWorker()).start();
            (hidCommandExecutor = new HIDCommandExecutor()).start();
        }
        // 获取剪切板内容
        else if (cmd == Command.GET_CLIPBOARD) {
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            Transferable content = clipboard.getContents(null);
            if (content.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                String text = (String) clipboard.getData(DataFlavor.stringFlavor);
                // 剪切板没有内容就别回应了
                if (text != null && text.length() > 0) {
                    byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
                    resp = Packet.create(Command.GET_CLIPBOARD_RESPONSE, 4 + bytes.length).addInt(bytes.length).addBytes(bytes);
                }
            }
        }
        // 设置剪切板内容
        else if (cmd == Command.SET_CLIPBOARD) {
            Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            int len = packet.nextInt();
            String text = new String(packet.nextBytes(len), StandardCharsets.UTF_8);
            StringSelection selection = new StringSelection(text);
            clipboard.setContents(selection, null);
            resp = Packet.create(Command.SET_CLIPBOARD_RESPONSE, 4).addBytes("OJBK".getBytes());
        }
        // 键鼠事件处理
        else if (cmd == Command.HID_COMMAND) {
            int hidType = packet.nextByte() & 0xff;
            int eventType = packet.nextByte() & 0xff;
            int key = packet.nextByte() & 0xff;
            short x = packet.nextShort();
            short y = packet.nextShort();
            int timestamp = packet.nextInt();
            HIDCommand hidCommand;
            if (hidType == HIDCommand.TYPE_MOUSE) {
                hidCommand = new MouseCommand(eventType, key, x, y, timestamp);
            } else {
                hidCommand = new KeyboardCommand(key, eventType, timestamp);
            }
            hidCommandExecutor.add(hidCommand);
        }
        // 停止远程控制
        else if (cmd == Command.CLOSE_REQUEST) {
            resp = Packet.create(Command.CLOSE_RESPONSE, 4).addBytes("OJBK".getBytes());
            working = false;
            captureWorker.terminate();
            compressWorker.terminate();
            hidCommandExecutor.terminate();
            heartBeatWorker.terminate();

            ScreenImages.clear();
        }
        // 列出文件列表
        else if (cmd == Command.LIST_FILES) {
            int len = packet.nextInt();
            String path = new String(packet.nextBytes(len), StandardCharsets.UTF_8);
            File[] files = FileSystem.list(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream(40960);
            baos.reset();
            for (int i = 0; files != null && i < files.length; i++) {
                File file = files[i];
                // 是否目录，长度，权限
                String name = file.getName();
                if ("".equals(name)) {
                    name = file.getAbsolutePath();
                }
                byte[] fbytes = name.getBytes(StandardCharsets.UTF_8);
                baos.write(file.isDirectory() ? 1 : 0);
                baos.write(ByteUtils.toBytes(file.length()));
                baos.write(ByteUtils.toBytes(file.lastModified()));
                baos.write(ByteUtils.toBytes(fbytes.length));
                baos.write(fbytes);
            }
            resp = Packet.create(Command.LIST_FILES_RESPONSE, baos.size());
            if (baos.size() > 0) {
                resp.addBytes(baos.toByteArray());
            }
        }
        // 传送文件到服务器端
        else if (cmd == Command.DOWNLOAD_FILE) {
            int pLength = packet.nextInt();
            String path = new String(packet.nextBytes(pLength), StandardCharsets.UTF_8);
            int nLength = packet.nextInt();
            String name = new String(packet.nextBytes(nLength), StandardCharsets.UTF_8);
            new FileTransferWorker(this, new File(new File(path), name)).start();
        } else if (cmd == Command.UPLOAD_FILE) {
            int seq = packet.nextInt();
            if (seq == 0) {

                if (fileWriter != null) {
                    fileWriter.close();
                }
                if (Objects.nonNull(bufferedFileWriter)) {
                    bufferedFileWriter.close();
                }


                restBytesToReceive = packet.nextLong();
                int pathLength = packet.nextInt();
                String filePath = new String(packet.nextBytes(pathLength), StandardCharsets.UTF_8);
                int nameLength = packet.nextInt();
                String fileName = new String(packet.nextBytes(nameLength), StandardCharsets.UTF_8);
                File file = new File(new File(filePath), fileName);
                bufferedFileWriter = new BufferedOutputStream(fileWriter = new FileOutputStream(file), 1024 * 1024);
            } else {
                int blockSize = packet.nextInt();
                restBytesToReceive -= blockSize;
                bufferedFileWriter.write(packet.nextBytes(blockSize));
                if (restBytesToReceive == 0) {
                    bufferedFileWriter.flush();
                    bufferedFileWriter.close();
                    fileWriter.close();
                }
            }

            resp = Packet.create(Command.UPLOAD_FILE_RESPONSE, 1).addByte((byte) 0x00);
        }

        // 发送响应至服务器端
        if (resp != null) {
            send(resp);
        }
    }

    public synchronized void send(Packet packet) throws IOException {
        packet.rewind().seek(6).nextByte();
        outputStream.write(packet.getBytes());
        outputStream.write(TAIL);
        outputStream.flush();
    }

    /**
     * 发送压缩后的屏幕截图
     *
     * @throws Exception Exception
     */
    private void sendScreenImages() throws Exception {
        if (!working) {
            return;
        }
        Packet p = ScreenImages.getCompressedScreen();
        assert p != null;
        send(p);
    }

    /**
     * 关闭连接，中断工作线程
     */
    private void release() {
        working = false;
        try {
            inputStream.close();
        } catch (Exception ignored) {
        }
        try {
            outputStream.close();
        } catch (Exception ignored) {
        }
        try {
            conn.close();
        } catch (Exception ignored) {
        }
        try {
            captureWorker.terminate();
        } catch (Exception ignored) {
        }
        try {
            compressWorker.terminate();
        } catch (Exception ignored) {
        }
    }

    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                converse();
            } catch (ConnectException e) {
                log.error(Objects.requireNonNull(ReadDataUtil.getTerminalConfig()).getIp() + "服务器远程控制模块Socket服务端离线");
                sleep(15000);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                release();
            }
            sleep(10);
        }
    }

    public Socket getConn() {
        return conn;
    }
}
